﻿using System;
using System.Linq;
using System.Net;
using HashFoo.Core;

namespace HashFoo.Rest.Common
{
    /// <summary>
    /// A web based client for queries and commands.
    /// </summary>
    /// <remarks>
    /// To auto marshal results back to a UI thread, supply the appropriate <see cref="IDispatcher"/> to the constructor.
    /// </remarks>
    public class ClientContext : IClientContext
    {
        readonly IWebClient _client;
        readonly IDispatcher _dispatcher;
        readonly IResourceSerializer _resourceSerializer;
        readonly IResourceUriCollection _resourceUriCollection;

        /// <summary>
        /// Create a new <see cref="ClientContext"/>
        /// </summary>
        /// <param name="client">The underlying <see cref="IWebClient"/> to use to make the web based calls.</param>
        /// <param name="resourceSerializer">The serializer to use</param>
        /// <param name="resourceUriCollection">The resource uris</param>
        /// <param name="dispatcher">Dispatcher to marshal calls back to a UI thread.  If null, no marshalling will be done.</param>
        /// <param name="credentialsProvider"> </param>
        /// <remarks>
        /// Right now, we only pass in a single <see cref="IResourceSerializer"/>, which implies that all calls are made using a
        /// single resource serialization method.  Some day, we should make it so that multiple serializers could be chosen from?
        /// </remarks>
        public ClientContext(
            IWebClient client,
            IResourceSerializer resourceSerializer,
            IResourceUriCollection resourceUriCollection,
            IDispatcher dispatcher = null,
            ICredentialsProvider credentialsProvider = null)
        {
            _client = client;
            _resourceSerializer = resourceSerializer;
            _resourceUriCollection = resourceUriCollection;
            _dispatcher = dispatcher ?? new FuncDispatcher { AsynInvoker = worker => worker.BeginInvoke(null, null) };

            // setup MIME for serializer.
            _client.DefaultHeaders[HttpRequestHeader.Accept] = _resourceSerializer.AcceptMimeTypes.Aggregate((c, n) => c + ", " + n);
            _client.DefaultHeaders[HttpRequestHeader.ContentType] = _resourceSerializer.ContentMimeType;

            // set up auth
            if (credentialsProvider != null && credentialsProvider.CurrentCredentials != null)
            {
                _client.DefaultHeaders[HttpRequestHeader.Authorization] =
                        WebUtil.CreateHttpBasicAuthHeader(credentialsProvider.CurrentCredentials.UserName, credentialsProvider.CurrentCredentials.Password);
            }
        }

        static Action<WebRequestError> EmptyErrorHandler
        {
            get { return e => { throw new Exception(e.StatusCode + "\n" + e.ServerResponse, e.ErrorException); }; }
        }

        /// <summary>
        /// Executes a command that returns a result.
        /// </summary>
        /// <typeparam name="TResultModel"></typeparam>
        /// <param name="command"></param>
        /// <param name="completedCallback"></param>
        /// <param name="errorCallback"></param>
        /// <param name="canceledCallback"></param>
        /// <returns></returns>
        public IWebClientOperation ExecuteCommand<TResultModel>(
            ICommand<TResultModel> command,
            Action<TResultModel> completedCallback,
            Action<WebRequestError> errorCallback = null,
            Action canceledCallback = null)
        {
            if (completedCallback == null) throw new ArgumentNullException("completedCallback", "a callback is required for an operation that returns a result.");

            var data = _resourceSerializer.Write(command);
            var uri = _resourceUriCollection.GetUri(command.GetType());

            return InternalUploadStringWithResult(completedCallback, errorCallback, canceledCallback, uri, data);
        }

        /// <summary>
        /// Executes a command that does not return a result.
        /// </summary>
        /// <param name="command"></param>
        /// <param name="completedCallback"></param>
        /// <param name="errorCallback"></param>
        /// <param name="canceledCallback"></param>
        /// <returns></returns>
        public IWebClientOperation ExecuteCommand(
            ICommand command,
            Action completedCallback = null,
            Action<WebRequestError> errorCallback = null,
            Action canceledCallback = null)
        {
            var data = _resourceSerializer.Write(command);
            var uri = _resourceUriCollection.GetUri(command.GetType());

            return InternalUploadStringNoResult(completedCallback, errorCallback, canceledCallback, uri, data);
        }

        /// <summary>
        /// Executes a query.
        /// </summary>
        /// <typeparam name="TResultModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="completedCallback"></param>
        /// <param name="errorCallback"></param>
        /// <param name="canceledCallback"></param>
        /// <returns></returns>
        public IWebClientOperation Query<TResultModel>(
            IQuery<TResultModel> query,
            Action<TResultModel> completedCallback,
            Action<WebRequestError> errorCallback = null,
            Action canceledCallback = null)
        {
            if (completedCallback == null) throw new ArgumentNullException("completedCallback");

            var data = _resourceSerializer.Write(query);
            var uri = _resourceUriCollection.GetUri(query.GetType());

            return InternalUploadStringWithResult(completedCallback, errorCallback, canceledCallback, uri, data);
        }

        /// <summary>
        /// Adds metadata to a query, then executes it.
        /// </summary>
        /// <typeparam name="TResultModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="meta"></param>
        /// <param name="completedCallback"></param>
        /// <param name="errorCallback"></param>
        /// <param name="canceledCallback"></param>
        /// <returns></returns>
        public IWebClientOperation Query<TResultModel>(
            IQuery<TResultModel> query,
            QueryMeta meta,
            Action<TResultModel> completedCallback,
            Action<WebRequestError> errorCallback = null,
            Action canceledCallback = null)
        {
            if (completedCallback == null) throw new ArgumentNullException("completedCallback");

            query.QueryMeta = meta;
            return Query(query, completedCallback, errorCallback, canceledCallback);
        }

        void HandleEmptyCallback(Action callback)
        {
            if (callback == null) return;
            _dispatcher.BeginInvoke(callback);
        }

        void HandleEmptyCallback<T>(Action<T> callback, T data, Action<T> emptyHandler = null)
        {
            if (callback == null)
            {
                if (emptyHandler != null) _dispatcher.BeginInvoke(() => emptyHandler(data));
                return;
            }

            _dispatcher.BeginInvoke(() => callback(data));
        }

        void HandleModelResult<TResultModel>(Action<TResultModel> callback, IWebResultString result)
        {
            if (callback == null) return;
            callback(_resourceSerializer.Read<TResultModel>(result.Data));
        }

        IWebClientOperation InternalUploadStringNoResult(Action completedCallback, Action<WebRequestError> errorCallback, Action canceledCallback, string uri, string data)
        {
            return _client.UploadStringAsync(
                new Uri(uri, UriKind.Relative),
                data,
                args => HandleEmptyCallback(completedCallback),
                err => HandleEmptyCallback(errorCallback, err, EmptyErrorHandler),
                () => HandleEmptyCallback(canceledCallback));
        }

        IWebClientOperation InternalUploadStringWithResult<TResult>(Action<TResult> completedCallback, Action<WebRequestError> errorCallback, Action canceledCallback, string uri, string data)
        {
            return _client.UploadStringAsync(
                new Uri(uri, UriKind.Relative),
                data,
                args => HandleModelResult<TResult>(x => _dispatcher.BeginInvoke(() => completedCallback(x)), args),
                err => HandleEmptyCallback(errorCallback, err, EmptyErrorHandler),
                () => HandleEmptyCallback(canceledCallback));
        }
    }
}